// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';
import 'package:compiler/src/commandline_options.dart';

Future<void> main(List<String> args) async {
  Stopwatch stopwatch = Stopwatch();
  String? input;
  String? serializedInput;
  String output = 'out.js';
  List<String> arguments = [];
  int start = 0;
  int stop = 3;
  int? shards;
  bool enableAssertions = false;
  for (String arg in args) {
    if (arg.startsWith('-')) {
      if (arg.startsWith('--start=')) {
        start = int.parse(arg.substring('--start='.length));
      } else if (arg.startsWith('--stop=')) {
        stop = int.parse(arg.substring('--stop='.length));
      } else if (arg.startsWith('--shards=')) {
        shards = int.parse(arg.substring('--shards='.length));
      } else if (arg == '-ea' || arg == '--enable_asserts') {
        enableAssertions = true;
      } else if (arg.startsWith('--in=')) {
        serializedInput = arg.substring('--in='.length);
      } else if (arg.startsWith('-o')) {
        output = arg.substring('-o'.length);
      } else if (arg.startsWith('--out=')) {
        output = arg.substring('--out='.length);
      } else {
        arguments.add(arg);
      }
    } else {
      if (input != null) {
        print("Multiple entrypoints provided: '$input' and '$arg'.");
        exit(-1);
      }
      input = arg;
    }
  }

  if (input == null) {
    print("No entrypoint provided.");
    exit(-1);
  }

  serializedInput ??= output;

  String inputPrefix = serializedInput;
  if (serializedInput.endsWith('.js')) {
    inputPrefix = output.substring(0, output.length - '.js'.length);
  }
  String outputPrefix = output;
  if (output.endsWith('.js')) {
    outputPrefix = output.substring(0, output.length - '.js'.length);
  }

  List<String> baseOptions = ['--packages=${Platform.packageConfig}'];
  if (enableAssertions) {
    baseOptions.add('--enable_asserts');
  }
  baseOptions.add('package:compiler/src/dart2js.dart');
  baseOptions.addAll(arguments);

  String cfeOutput = '${inputPrefix}0.dill';
  String dillOutput = '$inputPrefix.dill';
  String dataOutput = '$inputPrefix.dill.data';
  String codeOutput = '$outputPrefix.code';
  shards ??= 2;

  stopwatch.start();
  if (start <= 0 && stop >= 0) {
    await subProcess(baseOptions, [
      input,
      '${Flags.stage}=cfe',
      '--out=$cfeOutput',
    ], '0:\t');
  }
  if (start <= 1 && stop >= 1) {
    await subProcess(baseOptions, [
      cfeOutput,
      '--out=$dillOutput',
      '${Flags.closedWorldUri}=$dataOutput',
      '${Flags.stage}=closed-world',
    ], '1:\t');
  }
  if (shards <= 1) {
    await subProcess(baseOptions, [
      dillOutput,
      '${Flags.globalInferenceUri}=$dataOutput',
      '${Flags.stage}=codegen-emit-js',
      '--out=$output',
    ], '3:\t');
  } else {
    if (start <= 2 && stop >= 2) {
      List<List<String>> additionalArguments = [];
      List<String> outputPrefixes = [];
      for (int shard = 0; shard < shards; shard++) {
        additionalArguments.add([
          dillOutput,
          '${Flags.globalInferenceUri}=$dataOutput',
          '${Flags.codegenShard}=$shard',
          '${Flags.codegenShards}=$shards',
          '${Flags.codegenUri}=$codeOutput',
          '${Flags.stage}=codegen',
        ]);
        outputPrefixes.add('2:${shard + 1}/$shards\t');
      }

      Stopwatch subwatch = Stopwatch();
      subwatch.start();
      await Future.wait(
        List<Future<void>>.generate(shards, (int shard) {
          return subProcess(
            baseOptions,
            additionalArguments[shard],
            outputPrefixes[shard],
          );
        }),
      );
      subwatch.stop();
      print('2:\tTotal time: ${_formatMs(subwatch.elapsedMilliseconds)}');
    }
    if (start <= 3 && stop >= 3) {
      await subProcess(baseOptions, [
        dillOutput,
        '${Flags.globalInferenceUri}=$dataOutput',
        '${Flags.codegenUri}=$codeOutput',
        '${Flags.codegenShards}=$shards',
        '${Flags.stage}=emit-js',
        '--out=$output',
      ], '3:\t');
    }
  }
  stopwatch.stop();
  print('Total time: ${_formatMs(stopwatch.elapsedMilliseconds)}');
}

Future<void> subProcess(
  List<String> baseOptions,
  List<String> additionalOptions,
  String outputPrefix,
) async {
  List<String> options = [...baseOptions, ...additionalOptions];
  print(
    '${outputPrefix}Command: ${Platform.resolvedExecutable} ${options.join(' ')}',
  );
  Process process = await Process.start(
    Platform.resolvedExecutable,
    options,
    runInShell: true,
  );
  _Prefixer stdoutPrefixer = _Prefixer(outputPrefix, stdout);
  _Prefixer stderrOutputter = _Prefixer(outputPrefix, stderr);
  process.stdout.transform(utf8.decoder).listen(stdoutPrefixer.call);
  process.stderr.transform(utf8.decoder).listen(stderrOutputter.call);

  int exitCode = await process.exitCode;
  if (exitCode != 0) {
    exit(exitCode);
  }
}

class _Prefixer {
  final String _prefix;
  final Stdout _stdout;
  bool _atNewLine = true;

  _Prefixer(this._prefix, this._stdout);

  void call(String text) {
    int index = 0;
    while (index < text.length) {
      if (_atNewLine) {
        _stdout.write(_prefix);
        _atNewLine = false;
      }
      int pos = text.indexOf('\n', index);
      if (pos != -1) {
        _stdout.write(text.substring(index, pos + 1));
        _atNewLine = true;
        index = pos + 1;
      } else {
        _stdout.write(text.substring(index));
        index = text.length;
      }
    }
  }
}

String _formatMs(int ms) {
  return '${(ms / 1000).toStringAsFixed(3)}s';
}
